package org.foreningsgatan.modular.shadows.generator;

import java.util.LinkedList;
import java.util.List;

import org.foreningsgatan.modular.shadows.model.Obstacle;
import org.foreningsgatan.modular.shadows.model.Shadow;
import org.foreningsgatan.modular.shadows.model.ShadowCaster;

/**
 * 
 * Copyright (c) 2008 Joakim Back
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * @author Joakim Back
 *
 */
public class ShadowGenerator {
	public enum Modes {
		ALL, CLOSEST, AVERAGE
	}
	
	private Modes mMode;
	
	private double mAmbientAngle;
	
	private double mAmbientIntensity;
	
	public ShadowGenerator(Modes pModes) {
		this(pModes, 0, 0);
	}
	
	public ShadowGenerator(Modes pModes, double pAmbientAngle, double pAmbientIntensity) {
		mMode = pModes;
		mAmbientAngle = pAmbientAngle;
		mAmbientIntensity = pAmbientIntensity;
	}
	
	public List<Shadow> render(
			List<? extends ShadowCaster> pShadowCasters,
			List<? extends Obstacle> pObstacles) {
		switch (mMode) {
			case ALL:
				return renderAll(pShadowCasters, pObstacles);
			case CLOSEST:
				return renderClosest(pShadowCasters, pObstacles);
			case AVERAGE:
				return renderAverage(pShadowCasters, pObstacles);
			default: 
				return null;
		}
	}
	
	private List<Shadow> renderAll(
			List<? extends ShadowCaster> pShadowCasters,
			List<? extends Obstacle> pObstacles) {

		List<Shadow> vShadows = new LinkedList<Shadow>();

		for (ShadowCaster vCaster : pShadowCasters) {
			for (Obstacle vObs : pObstacles) {
				if (!vObs.hasShadow()) {
					continue;
				}
				
				double vDistance = getDistance(vCaster.getX(), vCaster.getY(),
						vObs.getX(), vObs.getY());

				if (vDistance <= vCaster.getRange()) {
					double vIntensity = 1.0 - (vDistance / vCaster.getRange());
					double vAngle = getAngle(vCaster.getX(), vCaster.getY(),
							vObs.getX(), vObs.getY());
					vShadows.add(new Shadow(vAngle, vObs, vIntensity));
				}
			}
		}

		return vShadows;
	}

	private List<Shadow> renderClosest(
			List<? extends ShadowCaster> pShadowCasters,
			List<? extends Obstacle> pObstacles) {
		List<Shadow> vShadows = new LinkedList<Shadow>();
		
		for (Obstacle vObs : pObstacles) {
			if (!vObs.hasShadow()) {
				continue;
			}
			
			double vClosestDistance = Double.MAX_VALUE;
			ShadowCaster vClosestCaster = null;
			
			for (ShadowCaster vCaster : pShadowCasters) {
				double vDistance = getDistance(vCaster.getX(), vCaster.getY(),
						vObs.getX(), vObs.getY());
				
				if ((vDistance <= vCaster.getRange()) && (vDistance < vClosestDistance)) {
					vClosestCaster = vCaster;
					vClosestDistance = vDistance;
				}
			}
			
			if (vClosestCaster != null) {
				double vIntensity = 1.0 - (vClosestDistance / vClosestCaster.getRange());
				double vAngle = getAngle(vClosestCaster.getX(), vClosestCaster.getY(),
						vObs.getX(), vObs.getY());
				vShadows.add(new Shadow(vAngle, vObs, vIntensity));
			}
		}
		
		return vShadows;
	}
	
	private List<Shadow> renderAverage(
			List<? extends ShadowCaster> pShadowCasters,
			List<? extends Obstacle> pObstacles) {
		List<Shadow> vShadows = new LinkedList<Shadow>();
		
		for (Obstacle vObs : pObstacles) {
			if (!vObs.hasShadow()) {
				continue;
			}
			
			double dX = Math.cos(mAmbientAngle) * mAmbientIntensity;
			double dY = Math.sin(mAmbientAngle) * mAmbientIntensity;
			
			for (ShadowCaster vCaster : pShadowCasters) {
				double vDistance = getDistance(vCaster.getX(), vCaster.getY(),
						vObs.getX(), vObs.getY());
				
				if (vDistance <= vCaster.getRange()) {
					double vIntensity = 1.0 - (vDistance / vCaster.getRange());
					double vAngle = getAngle(vCaster.getX(), vCaster.getY(),
							vObs.getX(), vObs.getY());
					
					dX += Math.cos(vAngle) * vIntensity;
					dY += Math.sin(vAngle) * vIntensity;
				}
			}
			
			double vAngle = Math.atan2(dY, dX);
			double vIntensity = getDistance(0, 0, dX, dY);
			
			if (vIntensity > 0) {
				vShadows.add(new Shadow(vAngle, vObs, Math.min(vIntensity, 1.0)));
			}
		}
		
		return vShadows;
	}

	private static double getDistance(double x1, double y1, double x2, double y2) {
		double vdX = (x2 - x1);
		double vdY = (y2 - y1);

		return Math.sqrt(vdX * vdX + vdY * vdY);
	}

	private static double getAngle(double x1, double y1, double x2, double y2) {
		double vDX = (x2 - x1);
		double vDY = (y2 - y1);

		return Math.atan2(vDY, vDX);
	}
}
